package com.bjh.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileFilter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;

public class ClassUtil {

    public static Logger log = LoggerFactory.getLogger(ClassUtil.class);

    public static final String FILE_PROTOCOL = "file";

    /**
     * 获取包下类的集合
     *
     * @param packageName  包名
     * @return   类集合
     */
    public static Set<Class<?>> extractPackageClass(String packageName) {

        //1:获取到类的加载器:目的是为了获取项目发布的实际路径，因为光通过用户提供的包路径com.coco，但是如何找到这个项目下的com.coco的目录呢？？
        //我们可以写绝对路径，但是不同机器目录是不一样的，所以不能写绝对路径，而且我们发布都是war或者jar根本找不到路径
        //所以我们需要通过类加载器去遍历包下的类，然后将类编译成class文件
        ClassLoader classLoader = getClassLoader();

        //2:通过类的加载器获取到加载的资源:因为用户传入的包名是 .  但是getResource接受的是 /

        URL url = classLoader.getResource(packageName.replace(".","/"));
        if(url == null) {
            log.warn("unable to retrieve anything from package:" + packageName);
            return null;
        }
        //3:依据不同的资源类型，采取不同的方式获取资源的集合
        Set<Class<?>> clazzSet = null;
        //过滤出文件资源类型
        if(url.getProtocol().equalsIgnoreCase(FILE_PROTOCOL)) {
            clazzSet = new HashSet<>();
            //获取文件目录
            File fileDirectory = new File(url.getPath());
            //然后可以遍历该目录下的所有文件
            extractClassFile(clazzSet, fileDirectory, packageName);
        }
        return clazzSet;
    }

    /**
     * 递归获取目标package里面所有的class文件(包括子package里的class文件)
     *
     * @param emptyclassSet 装载目标类的集合
     * @param fileSource  文件或者目录
     * @param packageName    包名
     */
    private static void extractClassFile(Set<Class<?>> emptyclassSet, File fileSource, String packageName) {
        if(!fileSource.isDirectory()) {
            return;
        }
        //如果是一个文件夹，则调用listFiles方法获取文件夹下的文件和文件夹
        File[] files = fileSource.listFiles(new FileFilter() {
            @Override
            public boolean accept(File file) {
                if(file.isDirectory()) {
                    return true;
                }else{
                    //获取文件的绝对路径
                    String absolutePath = file.getAbsolutePath();
                    if(absolutePath.endsWith(".class")) {
                        //如果是class文件，则直接加载
                        addToClassSet(absolutePath);
                    }
                }
                return false;
            }
            //根据class的绝对路径，获取并生成class对象，并放入集合当中
            private void addToClassSet(String absolutePath) {
                //1:从class文件的绝对路径里提取出包含了package的类名
                //如 /User/**/**/com/coco/people.class
                //需要变成com.coco.people
                //因为在Mac和linux路径是 /  但是window是 \  File.separator可以根据不同的操作系统区别，然后统一换成 . 号
                absolutePath = absolutePath.replace(File.separator,".");
                String className = absolutePath.substring(absolutePath.indexOf(packageName));
                className = className.substring(0,className.lastIndexOf("."));
                //2:通过反射机制获取对应的的class对象并加入到集合当中
                Class<?> aClass = loadClass(className);
                emptyclassSet.add(aClass);

            }
        });
        if(files != null) {
            for(File f : files) {
                //递归调用
                extractClassFile(emptyclassSet,f,packageName);
            }
        }
    }


    /**
     * 获取Class对象
     *
     * @param className  class全名 = package + 类名
     * @return
     */
    public static Class<?> loadClass(String className) {
        try {
            return Class.forName(className);
        } catch (ClassNotFoundException e) {
            log.error("load class error:",e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取classLoader实例
     * @return
     */
    public static ClassLoader getClassLoader() {
        return Thread.currentThread().getContextClassLoader();
    }

    /**
     * 设置类的属性值
     *
     * @param field 成员变量
     * @param target 类实例
     * @param value  成员变量的值
     * @param accessible 是否允许设置私有属性
     */
    public static void setField(Field field, Object target, Object value,boolean accessible){

        try {
            //是否允许设置私有的成员变量的值
            field.setAccessible(accessible);
            field.set(target,value);
        } catch (IllegalAccessException e) {
            log.error("setField error:" + e);
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        extractPackageClass("com.coco.entity");
    }
}
